﻿using System;
using System.Diagnostics;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Collections.Generic;
using System.Windows.Threading;


namespace SStuff.FlowControls
{
    [TemplatePart(Type = typeof(ItemsPresenter), Name = "ItemsPresenter")]
    [TemplateVisualState(GroupName = "Manipulation", Name = "Stabilized")]
    [TemplateVisualState(GroupName = "Manipulation", Name = "Manipulating")]
    [TemplateVisualState(GroupName = "Manipulation", Name = "Inertia")]
    public abstract class FlowItemsControlBase : ItemsControl
    {
        bool _stopSelectionEventPropagation = false;
        private bool _selectedIndexNeeded;
        private bool _selectedItemNeeded;


        Storyboard _layoutStoryBoard;
        DoubleAnimation _layoutOffsetAnim;

        protected FlowItemsControlBase()
        {
            _layoutOffsetAnim = new DoubleAnimation { EasingFunction = new CubicEase { EasingMode = System.Windows.Media.Animation.EasingMode.EaseInOut }, Duration = new Duration(TimeSpan.FromMilliseconds(500)) };
            Storyboard.SetTarget(_layoutOffsetAnim, this);
            Storyboard.SetTargetProperty(_layoutOffsetAnim, new PropertyPath(LayoutOffsetProperty));
            _layoutStoryBoard = new Storyboard { Children = { _layoutOffsetAnim } };

            this.LayoutUpdated+=OnLayoutUpdated;
            this.IsTabStop = true;
            //DispatcherTimer timer = new DispatcherTimer{Interval = _minTimeBetweenInertiaSteps};
            //timer.Stop();
            //timer.Tick += OnRendering;
            this.GetLoaded().Subscribe(() =>
                                           { InvalidateMeasure();
                                               CompositionTarget.Rendering += OnRendering;
                                           });
            this.GetUnloaded().Subscribe(
                () =>
                {
                    if(_layoutStoryBoard!=null)
                        _layoutStoryBoard.Pause();
                    CompositionTarget.Rendering -= OnRendering;
                });
        }

        private void OnRendering(object sender, EventArgs e)
        {
            if(ManipulationState == ManipulationState.Inertia)
            {
                if (Math.Abs(_currentManipulationData.Velocity) < MinVelocity)
                {
                    OnManipulationCompleted();
                    return;
                }

                var now = DateTime.Now;
                
                
                var current = _lastManipulationData;
                _lastManipulationData = _currentManipulationData;
                _currentManipulationData = current;

                var dt = (now - _lastManipulationData.Date).Ticks;
                var velocity = _lastManipulationData.Velocity*AttenuationRatio;
                var value = _lastManipulationData.Value + velocity*dt;
                _currentManipulationData.Date = now;
                _currentManipulationData.Value = value;
                _currentManipulationData.Velocity = velocity;


                if (!CyclicMode)
                {
                    if (LayoutOffset < 0 && _currentManipulationData.Velocity>0)
                        _currentManipulationData.Velocity = - 0.3 * _currentManipulationData.Velocity;
                    else if (LayoutOffset > Items.Count - 1 && _currentManipulationData.Velocity<0)
                        _currentManipulationData.Velocity = -0.3* _currentManipulationData.Velocity;

                }
                else
                {

                    var rounded = Math.Round(LayoutOffset);
                    if (rounded < 0)
                    {
                        LayoutOffset += Items.Count;

                    }
                    else if (rounded > Items.Count - 1)
                    {
                        LayoutOffset -= Items.Count;
                    }
                }
                var delta = _currentManipulationData.Value - _lastManipulationData.Value;
                LayoutOffset -= delta*0.5*GetConsideredItems();
                
                                           
            }
        }

        private void OnLayoutUpdated(object sender, EventArgs args)
        {
            var host = ItemsHost;
            if (host != null)
            {
                VirtualHost = host as VirtualizingPanel;
                this.LayoutUpdated -= OnLayoutUpdated;
            }
        }


        public bool IsWithin(DependencyObject element)
        {
            var candidate = element;
            while (candidate != null)
            {
                if (candidate == this)
                    return true;
                candidate = VisualTreeHelper.GetParent(candidate);
            }
            return false;
        }



        public ManipulationState ManipulationState
        {
            get { return (ManipulationState)GetValue(ManipulationStateProperty); }
            set { SetValue(ManipulationStateProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ManipulationState.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ManipulationStateProperty =
            DependencyProperty.Register("ManipulationState", typeof(ManipulationState), typeof(FlowItemsControlBase), new PropertyMetadata(ManipulationState.Stabilized,
                (s,a)=>
                    {
                        (s as FlowItemsControlBase).OnManipulationStateChanged();
                    }));

        private void OnManipulationStateChanged()
        {
            VisualStateManager.GoToState(this, ManipulationState.ToString(), true);
        }


        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);
            var focusedElement = FocusManager.GetFocusedElement() as DependencyObject;
            if (focusedElement == null)
            {
                Focus();
            }
            else
            {
                if (!IsWithin(focusedElement))
                    Focus();
            }
            
            if(this.HandleGestures && CaptureMouse())
            {
                OnManipulationStart(e.GetPosition(this));
            }
        }

        

        protected override void OnMouseMove(MouseEventArgs e)
        {
            base.OnMouseMove(e);
            if(ManipulationState == ManipulationState.Manipulating)
            {
                OnManipulationDelta(e.GetPosition(this));
            }
        }

        

        protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonUp(e);
            ReleaseMouseCapture();
            if (ManipulationState == ManipulationState.Manipulating)
                OnManipulationEnd(e.GetPosition(this));
        }

        private class ManipulationData
        {

            public DateTime Date { get; set; }
            public double Value { get; set; }
            public double Velocity
            {
                get; set;
            }

            public void CopyTo(ManipulationData lastManipulationData)
            {
                lastManipulationData.Date = Date;
                lastManipulationData.Value = Value;
                lastManipulationData.Velocity = Velocity;
            }
        }

        private ManipulationData _currentManipulationData = new ManipulationData();
        private ManipulationData _lastManipulationData = new ManipulationData();

        private void OnManipulationStart(Point mousePosition)
        {
            if (_layoutStoryBoard != null)
            {
                var current = LayoutOffset;
                _layoutStoryBoard.Stop();
                LayoutOffset = current;
            }
            ManipulationState = ManipulationState.Manipulating;
            _currentManipulationData.Date = DateTime.Now;
            _currentManipulationData.Velocity = 0;
            _currentManipulationData.Value = GetNormalizedGestureOffset(mousePosition);
            _currentManipulationData.CopyTo(_lastManipulationData);
        }

        protected abstract double GetNormalizedGestureOffset(Point mousePosition);
        protected abstract int GetConsideredItems();
        private const double MinVelocity = 0.00000001;
        private const double AttenuationRatio = 0.94;

        private void OnManipulationDelta(Point mousePosition)
        {
            var now = DateTime.Now;
            if (now != _currentManipulationData.Date)
            {
                var current = _lastManipulationData;
                _lastManipulationData = _currentManipulationData;
                _currentManipulationData = current;
                _currentManipulationData.Date = now;
                _currentManipulationData.Value = GetNormalizedGestureOffset(mousePosition);
                _currentManipulationData.Velocity = (_currentManipulationData.Value - _lastManipulationData.Value)/
                                                    (_currentManipulationData.Date - _lastManipulationData.Date).Ticks;
                var delta = _currentManipulationData.Value - _lastManipulationData.Value;
                
                var candidate = LayoutOffset - delta*0.5*GetConsideredItems();
                if(CyclicMode)
                {
                    LayoutOffset = candidate;
                }
                else
                {
                    if (candidate < 0)
                        candidate = 0;
                    else if (candidate > Items.Count -1)
                        candidate = Items.Count - 1;
                    LayoutOffset = candidate;
                }
            }
            else
            {

                var delta = GetNormalizedGestureOffset(mousePosition) - _currentManipulationData.Value;
                var candidate = LayoutOffset - delta * 0.5 * GetConsideredItems();
                if (CyclicMode)
                {
                    LayoutOffset = candidate;
                }
                else
                {
                    if (candidate < 0)
                        candidate = 0;
                    else if (candidate >= Items.Count)
                        candidate = Items.Count - 1;
                    LayoutOffset = candidate;
                }
            }
        }
        private void OnManipulationEnd(Point mousePosition)
        {

            _currentManipulationData.Date = DateTime.Now;
            _currentManipulationData.Velocity = (_currentManipulationData.Value - _lastManipulationData.Value) /
                                                (_currentManipulationData.Date - _lastManipulationData.Date).Ticks;
            ManipulationState = ManipulationState.Inertia;
        }
        
        private void OnManipulationCompleted()
        {
            while (Math.Round(LayoutOffset) <0)
            {
                LayoutOffset += Items.Count;

            }
            while(Math.Round(LayoutOffset)>=Items.Count)
            {
                LayoutOffset -= Items.Count;
            }
            SelectedIndex = (int) Math.Round(LayoutOffset);
            AnimateLayoutOffset(SelectedIndex);
            ManipulationState = ManipulationState.Stabilized;
        }
        protected override void OnLostMouseCapture(MouseEventArgs e)
        {
            base.OnLostMouseCapture(e);
            if(ManipulationState == ManipulationState.Manipulating)
                OnManipulationEnd(e.GetPosition(this));
        }
        protected abstract bool HandleGestures { get; }


        Panel _itemsHost;
        private Panel ItemsHost
        {
            get
            {
                // Lookup the ItemsHost if we haven't already cached it.
                if (_itemsHost == null && ItemContainerGenerator != null)
                {
                    // Get any live container
                    DependencyObject container = ItemContainerGenerator.ContainerFromIndex(0);
                    if (container != null)
                    {
                        // Get the parent of the container
                        _itemsHost = VisualTreeHelper.GetParent(container) as Panel;
                        if (_itemsHost != null) _itemsHost.InvalidateMeasure();
                    }
                }

                return _itemsHost;
            }
        }

        private void AnimateLayoutOffset(double to)
        {
            if (CyclicMode)
            {
                if (LayoutOffset + Items.Count - to < to - LayoutOffset)
                {
                    LayoutOffset += Items.Count;
                }
                else if (to + Items.Count - LayoutOffset < LayoutOffset - to)
                {
                    LayoutOffset -= Items.Count;
                }
            }

            TimeSpan perItemBasedDuration = TimeSpan.FromMilliseconds(AnimationDurationPerItem.TotalMilliseconds * Math.Abs(to - LayoutOffset));
            _layoutOffsetAnim.Duration = new Duration(perItemBasedDuration > MinimumAnimationDuration ? perItemBasedDuration : MinimumAnimationDuration);
            _layoutOffsetAnim.To = to;
            _layoutStoryBoard.Begin();
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            
        }

        protected override void ClearContainerForItemOverride(DependencyObject element, object item)
        {
            base.ClearContainerForItemOverride(element, item);
            FlowItemBase asFlowItemBase = element as FlowItemBase;
            if (asFlowItemBase != null)
            {
                asFlowItemBase.Content = null;
                asFlowItemBase.IsSelected = false;
            }
        }

        protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
        {
            base.PrepareContainerForItemOverride(element, item);
            var asItem = element as FlowItemBase;
            if (asItem != null)
            {
                asItem.Owner = this;
                if (SelectedItem == item)
                    asItem.IsSelected = true;
            }
            
        }

        protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            base.OnItemsChanged(e);
            if (_selectedIndexNeeded)
            {
                if (Items.Contains(SelectedItem))
                {
                    SelectedIndex = Items.IndexOf(SelectedItem);
                }
            }
            else if (_selectedItemNeeded)
            {
                if (Items.Count > SelectedIndex)
                {
                    SelectedItem = Items[SelectedIndex];
                }
            }
            else if(!Items.Contains(SelectedItem))
                SelectedItem = null;

            _selectedIndexNeeded = false;
            _selectedItemNeeded = false;
        }

        protected virtual void OnSelectedItemChanged()
        {
            if (_stopSelectionEventPropagation)
                return;

            _stopSelectionEventPropagation = true;

            if (SelectedIndex >= 0)
            {
                var selectedContainer = ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as FlowItemBase;
                if (selectedContainer != null)
                {
                    selectedContainer.IsSelected = false;
                }
            }

            if (SelectedItem == null)
            {
                SelectedIndex = -1;
            }
            else
            {
                if (Items.Contains(SelectedItem))
                {
                    SelectedIndex = Items.IndexOf(SelectedItem);
                    AnimateLayoutOffset(SelectedIndex);
                    var container = ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as FlowItemBase;
                    if (container != null)
                    {
                        container.IsSelected = true;
                    }
                }
                else
                {
                     // wait for the ItemsSource to be fetched
                    _selectedIndexNeeded = true;
                }
            }

            ManipulationState = ManipulationState.Stabilized;
            _stopSelectionEventPropagation = false;
        }
        protected virtual void OnSelectedIndexChanged()
        {
            if (_stopSelectionEventPropagation)
                return;

            _stopSelectionEventPropagation = true;
            if (SelectedItem != null)
            {
                var selectedContainer = ItemContainerGenerator.ContainerFromItem(SelectedItem) as FlowItemBase;
                if (selectedContainer != null)
                {
                    selectedContainer.IsSelected = false;
                }
            }
            if (SelectedIndex == -1)
            {
                SelectedItem = null;
            }
            else
            {
                if (Items.Count > SelectedIndex)
                {
                    SelectedItem = Items[SelectedIndex];
                    var container = ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as FlowItemBase;
                    if (container != null)
                    {
                        container.IsSelected = true;
                    }
                }
                else
                {
                    // wait for the ItemsSource to be fetched
                    _selectedItemNeeded = true;
                }
            }

            _stopSelectionEventPropagation = false;

            ManipulationState = ManipulationState.Stabilized;
            AnimateLayoutOffset(SelectedIndex == -1 ? 0.0 : (double)SelectedIndex);
        }

       

        protected virtual void OnVirtualHostChanged()
        {
        }



        protected virtual void OnLayoutOffsetChanged()
        {
            if (LayoutOffset == double.NegativeInfinity || LayoutOffset == double.PositiveInfinity)
                throw new ArgumentOutOfRangeException();
        }

        protected virtual void OnCyclicModeChanged()
        {
        }



        public bool CyclicMode
        {
            get { return (bool)GetValue(CyclicModeProperty); }
            set { SetValue(CyclicModeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for CyclicMode.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CyclicModeProperty =
            DependencyProperty.Register("CyclicMode", typeof(bool), typeof(FlowItemsControlBase), new PropertyMetadata(true, (s, a) =>
                {
                    (s as FlowItemsControlBase).OnCyclicModeChanged();
                }));






        public TimeSpan MinimumAnimationDuration
        {
            get { return (TimeSpan)GetValue(MinimumAnimationDurationProperty); }
            set { SetValue(MinimumAnimationDurationProperty, value); }
        }

        // Using a DependencyProperty as the backing store for MinimumAnimationDuration.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MinimumAnimationDurationProperty =
            DependencyProperty.Register("MinimumAnimationDuration", typeof(TimeSpan), typeof(FlowItemsControlBase), new PropertyMetadata(TimeSpan.FromMilliseconds(1000)));



        public TimeSpan AnimationDurationPerItem
        {
            get { return (TimeSpan)GetValue(AnimationDurationPerItemProperty); }
            set { SetValue(AnimationDurationPerItemProperty, value); }
        }

        // Using a DependencyProperty as the backing store for AnimationDurationPerItem.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AnimationDurationPerItemProperty =
            DependencyProperty.Register("AnimationDurationPerItem", typeof(TimeSpan), typeof(FlowItemsControlBase), new PropertyMetadata(TimeSpan.FromMilliseconds(400)));



        protected VirtualizingPanel VirtualHost
        {
            get { return (VirtualizingPanel)GetValue(VirtualHostProperty); }
            private set { SetValue(VirtualHostProperty, value); }
        }

        // Using a DependencyProperty as the backing store for VirtualHost.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty VirtualHostProperty =
            DependencyProperty.Register("VirtualHost", typeof(VirtualizingPanel), typeof(FlowItemsControlBase), new PropertyMetadata(null,
                (s, a) =>
                {
                    (s as FlowItemsControlBase).OnVirtualHostChanged();
                }));


        
        public object SelectedItem
        {
            get { return (object)GetValue(SelectedItemProperty); }
            set { SetValue(SelectedItemProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SelectedItem.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedItemProperty =
            DependencyProperty.Register("SelectedItem", typeof(object), typeof(FlowItemsControlBase), new PropertyMetadata(null,
                (s, a) =>
                {
                    (s as FlowItemsControlBase).OnSelectedItemChanged();
                }));

        public int SelectedIndex
        {
            get { return (int)GetValue(SelectedIndexProperty); }
            set { SetValue(SelectedIndexProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SelectedIndex.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedIndexProperty =
            DependencyProperty.Register("SelectedIndex", typeof(int), typeof(FlowItemsControlBase), new PropertyMetadata(-1,
                (s, a) =>
                {
                    (s as FlowItemsControlBase).OnSelectedIndexChanged();
                }));



        public double LayoutOffset
        {
            get { return (double)GetValue(LayoutOffsetProperty); }
            set { SetValue(LayoutOffsetProperty, value); }
        }

        // Using a DependencyProperty as the backing store for LayoutOffset.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LayoutOffsetProperty =
            DependencyProperty.Register("LayoutOffset", typeof(double), typeof(FlowItemsControlBase), new PropertyMetadata(0.0,
                (s, a) =>
                {
                    (s as FlowItemsControlBase).OnLayoutOffsetChanged();
                }));




        public KeyboardDirection KeyboardDirection
        {
            get { return (KeyboardDirection)GetValue(KeyboardDirectionProperty); }
            set { SetValue(KeyboardDirectionProperty, value); }
        }

        // Using a DependencyProperty as the backing store for KeyboardDirection.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty KeyboardDirectionProperty =
            DependencyProperty.Register("KeyboardDirection", typeof(KeyboardDirection), typeof(FlowItemsControlBase), new PropertyMetadata(KeyboardDirection.LeftRight));



        protected override void OnKeyDown(KeyEventArgs e)
        {
            base.OnKeyDown(e);
            if (!e.Handled)
            {
                switch (e.Key)
                {
                    case Key.Left:
                        {
                            if (KeyboardDirection.Contains(KeyboardDirection.LeftRight))
                            {
                                e.Handled = true;
                                SelectPrevious();
                            }
                        }
                        break;
                    case Key.Right:
                        {
                            if (KeyboardDirection.Contains(KeyboardDirection.LeftRight))
                            {
                                e.Handled = true;
                                SelectNext();
                            }
                        }
                        break;
                    case Key.Up:
                        {
                            if (KeyboardDirection.Contains(KeyboardDirection.UpDown))
                            {
                                e.Handled = true;
                                SelectPrevious();
                            }
                        }
                        break;
                    case Key.Down:
                        {
                            if (KeyboardDirection.Contains(KeyboardDirection.UpDown))
                            {
                                e.Handled = true;
                                SelectNext();
                            }
                        }
                        break;
                }
            }
        }

        private void SelectNext()
        {
            if (SelectedIndex == Items.Count - 1)
            {
                if(CyclicMode)
                    SelectedIndex = 0;
            }
            else
                SelectedIndex++;
        }

        private void SelectPrevious()
        {
            if (SelectedIndex == 0)
            {
                if (CyclicMode)
                    SelectedIndex = Items.Count - 1;
            }
            else
                SelectedIndex--;
        }

        


    }
}
